package com.xxxx.comment.service.impl;

import com.xxxx.comment.constants.RedisConstants;
import com.xxxx.comment.service.RedisDistributedLock;
import jakarta.annotation.Resource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 误删除锁的三种情况：
 * 1、我拿jmeter同时跑一百个线程进来，如果只根据业务名+id来唯一标识该用户的锁，然后对值只是随便设置
 *   如果不设置 业务名+用户ID，而是简单的业务名，这样可能会把其他用户的锁给删掉，导致异常
 * 2、将该线程对应UUID作为唯一标识保存为值，因为同一个用户的多个线程之间也可能出现误删除，
 *   所以还需要将线程的UUID作为唯一标识保存到值里面，删除的时候把值取出来判断一下是不是当前线程的锁
 * 3、更为极端的情况：线程1现在持有锁之后，在执行业务逻辑过程中，他正准备删除锁，而且已经走到了条件判断的过程中，
 *   比如他已经拿到了当前这把锁确实是属于他自己的，正准备删除锁，但是此时他的锁到期了，那么此时线程2进来，
 *   但是线程1他会接着往后执行，当他卡顿结束后，他直接就会执行删除锁那行代码，
 *   相当于条件判断并没有起到作用，这就是删锁时的原子性问题，
 *   之所以有这个问题，是因为线程1的拿锁，比锁，删锁，实际上并不是原子性的，我们要防止刚才的情况发生，
 *
 * @author lzf
 * @date 2023/9/1 10:41:11
 */
@Component(value = "redisLock")
public class RedisDistributedLockImpl implements RedisDistributedLock
{
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 线程的惟一标识，通过这个惟一标识来判断在多线程的环境下（例如：用户打开两个浏览器，同时点击下两个单），
     *   释放锁的时候，获取到的这个锁的值是不是当前的线程加的锁，从而不误删其他线程的锁
     */
    private static final String ID_PREFIX = UUID.randomUUID().toString();
    
    private static final DefaultRedisScript<Long> seckillUnlockScript;
    
    static {
        seckillUnlockScript = new DefaultRedisScript<>();
        seckillUnlockScript.setLocation(new ClassPathResource("seckillUnlock.lua"));
        seckillUnlockScript.setResultType(Long.class);
    }
    
    /**
     * 业务名称，存储到redis中的锁会在前面自动加上"redis:lock:"
     *
     * @param name 业务名称
     * @param timeoutSec 锁超时的时间
     * @return 加锁是否成功
     */
    @Override
    public boolean tryLock(String name, Integer timeoutSec)
    {
        // 获取当前线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        
        // 获取锁
        // RedisConstants.REDIS_LOCK_KEY + name,即redis锁前缀+业务名（业务名中包含用户ID）组成对应业务名的锁
        // threadId：对应键的值，代表对应线程ID+UUID生成对应的唯一标识
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue()
                .setIfAbsent(RedisConstants.REDIS_LOCK_KEY + name,
                        threadId, timeoutSec, TimeUnit.SECONDS));
    }
    
    /**
     * 误删除锁的两种情况：
     * 1、我拿jmeter同时跑一百个线程进来，如果只根据业务名+id来唯一标识该用户的锁，然后对值只是随便设置
     *   如果不设置 业务名+用户ID，而是简单的业务名，这样可能会把其他用户的锁给删掉，导致异常
     * 2、将该线程对应UUID作为唯一标识保存为值，因为同一个用户的多个线程之间也可能出现误删除，
     *   所以还需要将线程的UUID作为唯一标识保存到值里面，删除的时候把值取出来判断一下是不是当前线程的锁
     * 3、更为极端的情况：线程1现在持有锁之后，在执行业务逻辑过程中，他正准备删除锁，而且已经走到了条件判断的过程中，
     *   比如他已经拿到了当前这把锁确实是属于他自己的，正准备删除锁，但是此时他的锁到期了，那么此时线程2进来，
     *   但是线程1他会接着往后执行，当他卡顿结束后，他直接就会执行删除锁那行代码，
     *   相当于条件判断并没有起到作用，这就是删锁时的原子性问题，
     *   之所以有这个问题，是因为线程1的拿锁，比锁，删锁，实际上并不是原子性的，我们要防止刚才的情况发生，
     * @param name 业务名称
     */
    @Override
    public void unlock(String name)
    {
//        // 获取线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        // 获取锁中的标识
//        String id = stringRedisTemplate.opsForValue().get(RedisConstants.REDIS_LOCK_KEY + name);
//        // 判断标识是否一致
//        if (threadId.equals(id))
//            // 释放锁
//            stringRedisTemplate.delete(RedisConstants.REDIS_LOCK_KEY + name);
        
        // 改为调用lua脚本，实现 拿锁，比锁，删锁 这三个操作的原子性
        String key = RedisConstants.REDIS_LOCK_KEY + name;
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        stringRedisTemplate.execute(seckillUnlockScript, Collections.singletonList(key), threadId);
    }
}
